<?php

/*------------------------------------------------
*
*   Explay Engine Content Management System.
*   Copyright © 2008 Golovdinov Alexander.
*
*-------------------------------------------------
*
*   Official site: wwww.alex-home.spb.ru/explay.html
*   Contact e-mail: golovdinov@gmail.com
*
*   GNU General Public License original source:
*   http://www.gnu.org/licenses/gpl-3.0.html
*
------------------------------------------------*/

if (!defined ('EXPLAY')) exit;

$document_root = $_SERVER['DOCUMENT_ROOT'];
if (substr ($document_root, strlen($document_root) - 1) == '/') $document_root = substr ($document_root, 0, strlen($document_root)-1);
define ('DOCUMENT_ROOT', $document_root.'/');

include $_SERVER['DOCUMENT_ROOT'].'/engine/pclzip.lib.php';

class UpdateProcessException extends Exception {}

umask (0000);

/**
 * systemUpdate
 *
 * Обновление системы
 */
class systemUpdate {

	private $zip;
	private $errors = array ();
	private $cmsinfo = array ();
	private $info_file;
	private $log = array (); // ассоциативный массив действий. Он необходим для того, чтобы при повторе неудачного обновления не выполнять удачно совершенные действия
	public $update_info = array (); // ассоциативный массив удачных действий
	
	public $update_server = 'http://update.explay.su';
	
	/**
	 * Конструктор
	 *
	 * Определяет свойства системы (cmsinfo)
	 */
	public function __construct () {
		$this->info_file = DOCUMENT_ROOT.'/engine/cmsinfo.txt';
		$f = file ($this->info_file);
		
		foreach ($f as $s) {
			$s = explode ('=>', $s);
			$key = trim ($s[0]);
			
			$this->cmsinfo[$key] = trim ($s[1]);
		}

	}
	
	/**
	 * Проверка на доступные обновления
	 *
	 * @return array
	 */
	public function checkforupdate () {
		$version = urlencode ($this->cmsinfo['version']);
		$site = urlencode ($_SERVER['HTTP_HOST']);
		
		$xml_content = file_get_contents ($this->update_server.'/checkforupdate.xml?version='.$version.'&site='.$site);
		
		$xml = new SimpleXMLElement($xml_content);
		
		$response = array ();
		
		$responce['error'] = $xml->error;
		$responce['allowedversion'] = $xml->allowedversion;
		$responce['description'] = $xml->description;
		
		return $responce;
	}
	
	/**
	 * Запуск обновления
	 *
	 * @param strnig $tmp_file полный путь до архива с обновлением
	 **/
	public function run ($tmp_file) {
	
		if (!file_exists ($tmp_file)) throw new Exception ('Временный файл не найден или не загружен!');
		
		$this->zip = new PclZip($tmp_file);
		
		// Извлекаем карту обновлений
		$map = $this->zip->extract(PCLZIP_OPT_BY_NAME, 'update.xml', PCLZIP_OPT_EXTRACT_AS_STRING);
	
		if (count($map) == 0 || !is_array ($map)) throw new Exception ('Нет карты обновления или ошибка распаковки zip-архива');
		$map = $map[0]['content'];
		
		// Если попытка обновления уже была, то пытаемся загрузить его карту с логами
		$update_map_log = $_SERVER['DOCUMENT_ROOT'].'/update/update_log.xml';
		
		if (file_exists ($update_map_log)) {
		
			$log_map = file_get_contents ($update_map_log);
			
			$log_map = new SimpleXMLElement($log_map);
			
			foreach ($log_map->actions[0]->action as $action_log) {
				$summ = (string) $action_log->md5summ;
				$astatus = (string) $action_log->status;
				$status = ($astatus == 'true') ? true : false;
				
				// Записываем действие в лог
				$this->log[$summ] = $status;
			}
			
			unset ($log_map);
		}
		
		
		// Разбираем карту из архива с обновлением
		$xml = new SimpleXMLElement($map);
		
		$allowed_version = (string) $xml->allowed;
		if ($allowed_version !== $this->cmsinfo['version']) throw new Exception ('Вы пытаетесь установить обновление, несовместимое с вашей версией!');
		
		foreach ($xml->actions[0]->action as $action) {
			$type = (string) $action->type;
			
			switch ($type) {
			
				// Замена файла
				case 'movefile' : {
					$tmp = (string) $action->tmp;
					$server = (string) $action->server;
					$md5summ = md5 ($tmp.$server); // определяем уникальный ключ действия
					
					// Если такого действия не было или оно было выполнено с ошибкой, то выполняем его
					if (!isset($this->log[$md5summ]) || $this->log[$md5summ] == false) {
						try {
							$this->movefile ($tmp, $server);
							$this->log[$md5summ] = true; // действие выполнено успешно - больше никогда не повторится
						}
						catch (UpdateProcessException $e) {
							array_push ($this->errors, $e->getMessage());
							$this->log[$md5summ] = false; // действие выполнено с ошибкой - будет повторятся
						}
					}
					
					break;
				}
				
				// Создание папки
				case 'createdir' : {
					$name = (string) $action->name;
					$md5summ = md5 ($name);
					
					if (!isset($this->log[$md5summ]) || $this->log[$md5summ] == false) {
						try {
							$this->createdir ($name);
							$this->log[$md5summ] = true;
						}
						catch (UpdateProcessException $e) {
							array_push ($this->errors, $e->getMessage());
							$this->log[$md5summ] = false;
						}
					}
					
					break;
				}
				
				// Выполнение MySQL-запроса
				case 'mysqlquery' : {
					$query = (string) $action->sql;
					$md5summ = md5 ($query);
					
					if (!isset($this->log[$md5summ]) || $this->log[$md5summ] == false) {
						try {
							$this->mysqlquery ($query);
							$this->log[$md5summ] = true;
						}
						catch (UpdateProcessException $e) {
							array_push ($this->errors, $e->getMessage());
							$this->log[$md5summ] = false;
						}
					}
					
					break;
				}
				
				// Выполнение PHP-кода
				case 'php' : {
					$php = (string) $action->php;
					$md5summ = md5 ($php);
					
					if (!isset($this->log[$md5summ]) || $this->log[$md5summ] == false) {
						$this->php ($php);
						$this->log[$md5summ] = true;
					}
					
					break;
				}
				
				// Изменение свойств системы
				case 'cmsinfo' : {
					$key = (string) $action->key;
					$value = (string) $action->value;
					$md5summ = md5 ($key.$value);
					
					if (!isset($this->log[$md5summ]) || $this->log[$md5summ] == false) {
						try {
							$this->change_cmsinfo ($key, $value);
							$this->log[$md5summ] = true;
						}
						catch (UpdateProcessException $e) {
							array_push ($this->errors, $e->getMessage());
							$this->log[$md5summ] = false;
						}
					}
					
					break;
				}
				
				default : {
					array_push ($this->errors, 'Действие "'.$type.'" не индетифицировано!');
					break;
				}
			}
		}
		
		// Создаем лог-карту
		$map = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<actionsmap>\r\n\t<actions>";
		
		foreach ($this->log as $summ => $status) {
			$map .= "\r\n\t\t<action>".
				"\r\n\t\t\t<md5summ>".$summ."</md5summ>".
				"\r\n\t\t\t<status>".(($status) ? 'true' : 'false')."</status>".
				"\r\n\t\t</action>";
		}
		
		$map .= "\r\n\t</actions>\r\n</actionsmap>";
		
		$f = fopen ($update_map_log, 'w+');
		fwrite ($f, $map);
		fclose ($f);
		
		// Если при обновлеии возникли ошибки
		if (count($this->errors) > 0) {
			$errors = '';
			foreach ($this->errors as $error) {
				$errors .= "\n<li>".$error;
			}
			throw new Exception ('<ul>'.$errors.'</ul>');
		}
		
		// Сохраняем настройки
		$this->cmsinfo['lastupdate'] = date ('d-m-Y H:i');
		
		$this->write_cmsinfo ();		
	}
	
	/**
	 * Копирование файла из архива на сервер
	 *
	 * Если файл существует, то он будет перезаписан,
	 * в противоположном случае будет создан новый
	 * файл
	 *
	 * @param string $tmp адрес файла в архиве
	 * @param string $server относительный путь к файлу на сервере
	 */
	private function movefile ($tmp, $server) {
		$tmp_content = $this->zip->extract(PCLZIP_OPT_BY_NAME, $tmp, PCLZIP_OPT_EXTRACT_AS_STRING);
		
		if (!is_array ($tmp_content) || count ($tmp_content) == 0) throw new UpdateProcessException ('Ошибка при извлечении "'.$tmp.'"!');
		
		$tmp_content = $tmp_content[0]['content'];
		
		$server = DOCUMENT_ROOT.$server;
		
		if (file_exists ($server)) {
			@chmod ($server, 0777);
			if (!is_writable ($server)) throw new UpdateProcessException ('Невозможно обновить "'.$server.'" - нет прав на запись!');
		} else {
			$pathinfo = pathinfo ($server);
			@chmod ($pathinfo['dirname'], 0777);
			if (!is_writable ($pathinfo['dirname'])) throw new UpdateProcessException ('Невозможно записать в "'.$pathinfo['dirname'].'" - нет прав на запись!');
		}
		
		$file = fopen ($server, 'w');
		flock ($file, LOCK_EX);
		fwrite ($file, $tmp_content);
		flock ($file, LOCK_UN);
		fclose ($file);
		
		@chmod ($server, 0777);
		
		array_push ($this->update_info, 'Обновлен файл <em>"'.$server.'"</em>');
	}
	
	/**
	 * Создание папки
	 *
	 * @param string $name относительный путь к новой папке на сервере
	 */
	private function createdir ($name) {
		
		$name = DOCUMENT_ROOT.$name;
		
		if (!mkdir ($name, 0777)) throw new UpdateProcessException ('Невожможно создать папку "'.$name.'"');
		
		array_push ($this->update_info, 'Создана директория <em>"'.$name.'"</em>');
	}
	
	/**
	 * Выполнение MySQL-запроса
	 *
	 * @param string $query запрос
	 */
	private function mysqlquery ($query) {
		if (!@mysql_query ($query)) throw new UpdateProcessException ('ошибка MySQL: '.mysql_error ());
	}
	
	/**
	 * Выполнение PHP-кода
	 *
	 * @param string $php php-код
	 */
	private function php ($code) {
		eval ($code);
	}
	
	/**
	 * Изменение настроек системы
	 */
	private function change_cmsinfo ($key, $value) {
		$this->cmsinfo[$key] = $value;
	}
	
	/**
	 * Запись обновленных настроек
	 */
	private function write_cmsinfo () {
		
		if (!is_writable ($this->info_file)) throw new Exception ('Нет прав на запись в "'.$this->info_file.'"');
		
		$new = "title=>{$this->cmsinfo['title']}
version=>{$this->cmsinfo['version']}
site=>{$this->cmsinfo['site']}
developer=>{$this->cmsinfo['developer']}
signature=>{$this->cmsinfo['signature']}
user-agent=>{$this->cmsinfo['user-agent']}
lastupdate=>{$this->cmsinfo['lastupdate']}
";

		$f = fopen ($this->info_file, 'w+');
		fwrite ($f, $new);
		fclose ($f);
	}
}